說到 條件型別
會聯想到 infer
extends
三元運算子
這三個關鍵字
本文除了介紹「條件型別」的概念外,也會提到條件型別的約束(constraints)、推斷(infer)、可分配性(distributive)
在 TypeScript 中 extends
關鍵字除了用在 interface 和 Class 的繼承外,還可以用在「條件型別」
「條件型別」中的 extends
是允許我們可以根據某個條件來選擇兩種型別中的其中一個
條件型別的基本語法
A extends B ? C : D
A
型別是否可以賦值給 B
型別。這裡的「是否可以賦值給」等同於「A 是否是 B 的子型別」、「A 是否兼容 B」C
型別,否則則是 D
型別interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
type Example1 = Dog extends Animal ? number : string; // ✅ type Example1 = number
type Example2 = RegExp extends Animal ? number : string; // ✅ type Example2 = string
以範例中的 type Example1
來說
Dog 是 Animal 的子型別,所以 type Example1
的結果是 number
至於 Example2
RegExp 不是 Animal 的子型別,所以 type Example2
的結果是 string
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}
從 createLabel
函式中我們不難看出
number
時,會回傳 IdLabel
string
時,會回傳 NameLabel
string | number
時,可以回傳 IdLabel | NameLabel
但這也造成一些問題,增加後續維護的負擔
條件型別的真正威力在於能與 generics 泛型
結合使用,這使函式可以根據輸入的型別來動態決定回傳的型別
上述 code 簡化後如下(想複習觀念的推薦也可以試著先自己來簡化看看喔!)
interface IdLabel {
id: number;
}
interface NameLabel {
name: string;
}
// 條件型別的定義
type NameOrId<T extends number | string> = T extends number ? IdLabel : NameLabel;
// 條件型別的實現
function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
throw "unimplemented";
}
let a = createLabel("typescript"); // ✅ let a: NameLabel
let b = createLabel(2.8); // ✅ let b: IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // ✅ let c: NameLabel | IdLabel
這裡使用條件型別結合「generics 泛型」的方式定義一個型別別名,並把多個重載 (overloads) 的函式簡化為沒有重載的單一函式
來拆解一下上面這段簡化後的 code
NameOrId<T>
為條件型別<T extends number | string>
:泛型 T 必須是 number 或 string 的子類型。確保只有數字和字符串可以作為這個條件型別的輸入T extends number ? IdLabel : NameLabel
:如果 T 是 number 的子型別,則回傳 IdLabel 型別,否則則回傳 NameLabel 型別type MessageOf<T> = T["message"]; // ❌ Type '"message"' cannot be used to index type 'T'.
在上面這個例子中,TypeScript 出錯是因為它不知道泛型 T
是否擁有 message
的屬性。我們可以透過約束(constraints) T 的結構,讓錯誤消失
PS. T["message"]
是「索引訪問」
type MessageOf<T extends { message: unknown }> = T["message"];
MessageOf<T extends { message: unknown }>
T 繼承了 { message: unknown }
,這表示 T 必須是包含 message
屬性的「物件」。只要擁有 message 屬性的物件才能作為 MessageOf<T>
的泛型參數
如此一來我們就能確認 message 屬性是否存在惹~
接著來使用看看
type MessageOf<T extends { message: unknown }> = T["message"];
interface Email {
message: string;
}
type EmailMessageContents = MessageOf<Email>;
最後一行的 MessageOf<Email>
會取得 Email 介面中 message 的型別。因此 EmailMessageContents 的型別也會是 string
最後是來加入條件型別
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
interface Email {
message: string;
}
interface Dog {
bark(): void;
}
type EmailMsgContents = MessageOf<Email>; // ✅ type EmailMsgContents = string
type DogMsgContents = MessageOf<Dog>; // ✅ type DogMessageContents = never
假設你正在玩一個猜物品的遊戲,其中每個盒子裡都裝了不同的物品,在不打開盒子的情況下,你只能根據盒子外面的提示來猜測盒子裡面的物品可能是什麼
在 TypeScript 裡,infer
是個偷吃步(?,讓你能夠打開盒子並直接看到裡面的物品是什麼
我們不需要事先知道所有的具體型別,這就像是在不知道盒子裡具體有什麼的情況下,依然能夠查看裡面的物品
條件型別的推斷會使用 infer
關鍵字
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;
type NumbersArray = Flatten<number[]>; // ✅ type NumbersArray = number
type StringVal = Flatten<string>; // ✅ type StringVal = string
Type extends Array<infer Item> ? Item : Type
:表示如果 Type 是一個 array,那就推斷出這個 array 的元素型別,並將其儲存於 Item,這個 Item 可以隨意替換不同名稱
「條件型別的可分配性」是指條件型別可以逐一應用於聯合型別中的每個成員,然後將結果再次組合成一個新的聯合型別
假設今天有一個盒子,裏面放了 🔴 紅色、🔵 藍色、🟡 黃色 的球
「紅色球」代表「Yes」,「非紅色球」代表「No」
Q:盒子裡的球是否是紅色
按照分配性,會這樣回答
A:
紅色球? "Yes"
藍色球? "No"
黃色球? "No"
不論如何,你的答案只會是 "Yes" 或 "No"
在 TypeScript 中,當對聯合型別(比如 string | number | boolean)使用條件型別時,TypeScript 會對每個可能的型別 (string, number, boolean)
分別應用這個條件,並給出每個情況的結果,最後把這些結果組合起來,形成一個新的聯合型別 ('Yes' | 'No')
以上這是說明可分配性的「概念」
轉換成 code 如下:
type IsString<T> = T extends string ? 'Yes' : 'No';
// 聯合型別
type MixedTypes = string | number | boolean;
// 聯合型別 結合「條件型別」
type Result = IsString<MixedTypes>; // Result 的型別會是 'Yes' | 'No'
如果不希望條件型別具有分配性,可以將型別封裝在「元組」中。這樣 TypeScript 就會將整個聯合型別作為一個單一實體來處理,而不是分別處理每個成員
type NonDistributive<T> = [T] extends [string] ? 'Yes' : 'No';
每天的內容有推到 github 上喔